/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nutch.plugin;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.slf4j.Logger;

import org.apache.hadoop.conf.Configuration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * The <code>PluginManifestParser</code> parser just parse the manifest file in
 * all plugin directories.
 * 
 * @author joa23
 */
public class PluginManifestParser {
	private static final String ATTR_NAME = "name";
	private static final String ATTR_CLASS = "class";
	private static final String ATTR_ID = "id";

	public static final Logger LOG = PluginRepository.LOG;

	private static final boolean WINDOWS = System.getProperty("os.name")
			.startsWith("Windows");

	private Configuration conf;

	private PluginRepository pluginRepository;

	public PluginManifestParser(Configuration conf,
			PluginRepository pluginRepository) {
		this.conf = conf;
		this.pluginRepository = pluginRepository;
	}

	/**
	 * Returns a list of all found plugin descriptors.
	 * 
	 * @param pluginFolders
	 *            folders to search plugins from
	 * @return A {@link Map} of all found {@link PluginDescriptor}s.
	 */
	public Map<String, PluginDescriptor> parsePluginFolder(
			String[] pluginFolders) {
		Map<String, PluginDescriptor> map = new HashMap<String, PluginDescriptor>();

		if (pluginFolders == null) {
			throw new IllegalArgumentException("plugin.folders is not defined");
		}

		for (String name : pluginFolders) {
			File directory = getPluginFolder(name);
			if (directory == null) {
				continue;
			}
			LOG.info("Plugins: looking in: " + directory.getAbsolutePath());
			for (File oneSubFolder : directory.listFiles()) {
				if (oneSubFolder.isDirectory()) {
					String manifestPath = oneSubFolder.getAbsolutePath()
							+ File.separator + "plugin.xml";
					try {
						LOG.debug("parsing: " + manifestPath);
						PluginDescriptor p = parseManifestFile(manifestPath);
						map.put(p.getPluginId(), p);
					} catch (Exception e) {
						LOG.warn("Error while loading plugin `" + manifestPath
								+ "` " + e.toString());
					}
				}
			}
		}
		return map;
	}

	/**
	 * Return the named plugin folder. If the name is absolute then it is
	 * returned. Otherwise, for relative names, the classpath is scanned.
	 */
	public File getPluginFolder(String name) {
		File directory = new File(name);
		if (!directory.isAbsolute()) {
			URL url = PluginManifestParser.class.getClassLoader()
					.getResource(name);
			if (url == null && directory.exists() && directory.isDirectory()
					&& directory.listFiles().length > 0) {
				return directory; // relative path that is not in the classpath
			} else if (url == null) {
				LOG.warn("Plugins: directory not found: " + name);
				return null;
			} else if (!"file".equals(url.getProtocol())) {
				LOG.warn("Plugins: not a file: url. Can't load plugins from: "
						+ url);
				return null;
			}
			String path = url.getPath();
			if (WINDOWS && path.startsWith("/")) // patch a windows bug
				path = path.substring(1);
			try {
				path = URLDecoder.decode(path, "UTF-8"); // decode the url path
			} catch (UnsupportedEncodingException e) {
			}
			directory = new File(path);
		} else if (!directory.exists()) {
			LOG.warn("Plugins: directory not found: " + name);
			return null;
		}
		return directory;
	}

	/**
	 * @param manifestPath
	 * @throws ParserConfigurationException
	 * @throws IOException
	 * @throws SAXException
	 * @throws MalformedURLException
	 */
	private PluginDescriptor parseManifestFile(String pManifestPath)
			throws MalformedURLException, SAXException, IOException,
			ParserConfigurationException {
		Document document = parseXML(new File(pManifestPath).toURI().toURL());
		String pPath = new File(pManifestPath).getParent();
		return parsePlugin(document, pPath);
	}

	/**
	 * @param url
	 * @return Document
	 * @throws IOException
	 * @throws SAXException
	 * @throws ParserConfigurationException
	 * @throws DocumentException
	 */
	private Document parseXML(URL url)
			throws SAXException, IOException, ParserConfigurationException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();
		return builder.parse(url.openStream());
	}

	/**
	 * @param pDocument
	 * @throws MalformedURLException
	 */
	private PluginDescriptor parsePlugin(Document pDocument, String pPath)
			throws MalformedURLException {
		Element rootElement = pDocument.getDocumentElement();
		String id = rootElement.getAttribute(ATTR_ID);
		String name = rootElement.getAttribute(ATTR_NAME);
		String version = rootElement.getAttribute("version");
		String providerName = rootElement.getAttribute("provider-name");
		String pluginClazz = null;
		if (rootElement.getAttribute(ATTR_CLASS).trim().length() > 0) {
			pluginClazz = rootElement.getAttribute(ATTR_CLASS);
		}
		PluginDescriptor pluginDescriptor = new PluginDescriptor(id, version,
				name, providerName, pluginClazz, pPath, this.conf);
		LOG.debug("plugin: id=" + id + " name=" + name + " version=" + version
				+ " provider=" + providerName + "class=" + pluginClazz);
		parseExtension(rootElement, pluginDescriptor);
		parseExtensionPoints(rootElement, pluginDescriptor);
		parseLibraries(rootElement, pluginDescriptor);
		parseRequires(rootElement, pluginDescriptor);
		return pluginDescriptor;
	}

	/**
	 * @param pRootElement
	 * @param pDescriptor
	 * @throws MalformedURLException
	 */
	private void parseRequires(Element pRootElement,
			PluginDescriptor pDescriptor) throws MalformedURLException {

		NodeList nodelist = pRootElement.getElementsByTagName("requires");
		if (nodelist.getLength() > 0) {

			Element requires = (Element) nodelist.item(0);

			NodeList imports = requires.getElementsByTagName("import");
			for (int i = 0; i < imports.getLength(); i++) {
				Element anImport = (Element) imports.item(i);
				String plugin = anImport.getAttribute("plugin");
				if (plugin != null) {
					pDescriptor.addDependency(plugin);
				}
			}
		}
	}

	/**
	 * @param pRootElement
	 * @param pDescriptor
	 * @throws MalformedURLException
	 */
	private void parseLibraries(Element pRootElement,
			PluginDescriptor pDescriptor) throws MalformedURLException {
		NodeList nodelist = pRootElement.getElementsByTagName("runtime");
		if (nodelist.getLength() > 0) {

			Element runtime = (Element) nodelist.item(0);

			NodeList libraries = runtime.getElementsByTagName("library");
			for (int i = 0; i < libraries.getLength(); i++) {
				Element library = (Element) libraries.item(i);
				String libName = library.getAttribute(ATTR_NAME);
				NodeList list = library.getElementsByTagName("export");
				Element exportElement = (Element) list.item(0);
				if (exportElement != null)
					pDescriptor.addExportedLibRelative(libName);
				else
					pDescriptor.addNotExportedLibRelative(libName);
			}
		}
	}

	/**
	 * @param rootElement
	 * @param pluginDescriptor
	 */
	private void parseExtensionPoints(Element pRootElement,
			PluginDescriptor pPluginDescriptor) {
		NodeList list = pRootElement.getElementsByTagName("extension-point");
		if (list != null) {
			for (int i = 0; i < list.getLength(); i++) {
				Element oneExtensionPoint = (Element) list.item(i);
				String id = oneExtensionPoint.getAttribute(ATTR_ID);
				String name = oneExtensionPoint.getAttribute(ATTR_NAME);
				String schema = oneExtensionPoint.getAttribute("schema");
				ExtensionPoint extensionPoint = new ExtensionPoint(id, name,
						schema);
				pPluginDescriptor.addExtensionPoint(extensionPoint);
			}
		}
	}

	/**
	 * @param rootElement
	 * @param pluginDescriptor
	 */
	private void parseExtension(Element pRootElement,
			PluginDescriptor pPluginDescriptor) {
		NodeList extensions = pRootElement.getElementsByTagName("extension");
		if (extensions != null) {
			for (int i = 0; i < extensions.getLength(); i++) {
				Element oneExtension = (Element) extensions.item(i);
				String pointId = oneExtension.getAttribute("point");

				NodeList extensionImplementations = oneExtension
						.getChildNodes();
				if (extensionImplementations != null) {
					for (int j = 0; j < extensionImplementations
							.getLength(); j++) {
						Node node = extensionImplementations.item(j);
						if (!node.getNodeName().equals("implementation")) {
							continue;
						}
						Element oneImplementation = (Element) node;
						String id = oneImplementation.getAttribute(ATTR_ID);
						String extensionClass = oneImplementation
								.getAttribute(ATTR_CLASS);
						LOG.debug("impl: point=" + pointId + " class="
								+ extensionClass);
						Extension extension = new Extension(pPluginDescriptor,
								pointId, id, extensionClass, this.conf,
								this.pluginRepository);
						NodeList parameters = oneImplementation
								.getElementsByTagName("parameter");
						if (parameters != null) {
							for (int k = 0; k < parameters.getLength(); k++) {
								Element param = (Element) parameters.item(k);
								extension.addAttribute(
										param.getAttribute(ATTR_NAME),
										param.getAttribute("value"));
							}
						}
						pPluginDescriptor.addExtension(extension);
					}
				}
			}
		}
	}
}
